package com.stars.easyms.rest.initializer;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.stars.easyms.rest.bean.RestInfo;
import com.stars.easyms.rest.exception.RestRuntimeException;
import org.springframework.lang.Nullable;

import java.util.*;

/**
 * <p>className: RequestMappingPathForRestInfo</p>
 * <p>description: 不同的method和methodFor对应不同的RestInfo，相同的requestMappingPath加密信息保证一致</p>
 *
 * @author guoguifang
 * @version 1.2.2
 * @date 2019-07-18 17:17
 */
public final class RequestMappingPathForRestInfo {

    private final String requestMappingPath;

    private RestInfo firstRestInfo;

    private RestInfo defaultRestInfo;

    private TreeMap<Integer, Set<RestInfoMethodRelation>> priorityRestInfoMethodRelationMap;

    /**
     * 为requestMappingPath添加RestInfo映射信息
     *
     * @param restInfo restInfo信息
     * @return 需要注册的映射路径
     */
    @SuppressWarnings("unchecked")
    String add(RestInfo restInfo) {
        // 先检查加密信息是否一致
        checkEncryptRestInfo(restInfo);

        // 获取methodMap，如果为空或其size为0则直接设置成默认的，如果默认有值则抛出异常
        Map<String, String> methodMap = restInfo.getMethodMap();
        if (methodMap == null || methodMap.isEmpty()) {
            if (defaultRestInfo != null) {
                throw new RestRuntimeException("The restController[{}] method[{}] register requestMappingPath[{}] fail, " +
                        "because it's already registered by the restController[{}] method[{}]!",
                        restInfo.getRestControllerClass().getCanonicalName(), restInfo.getRestControllerMethod().getName(), requestMappingPath,
                        defaultRestInfo.getRestControllerClass().getCanonicalName(), defaultRestInfo.getRestControllerMethod().getName());
            }
            defaultRestInfo = restInfo;

            // 如果没有method和methodFor则使用默认的requestMappingPath
            saveEncryptRestInfo(restInfo);
            return requestMappingPath;
        }

        // 懒加载Map对象，不存在多线程调用无需加锁
        if (priorityRestInfoMethodRelationMap == null) {
            priorityRestInfoMethodRelationMap = new TreeMap<>();
        }
        Set<RestInfoMethodRelation> restInfoMethodRelationSet = priorityRestInfoMethodRelationMap.computeIfAbsent(methodMap.size(), key -> new HashSet<>());
        RestInfoMethodRelation restInfoMethodRelation = new RestInfoMethodRelation(methodMap, restInfo);
        if (restInfoMethodRelationSet.contains(restInfoMethodRelation)) {
            RestInfoMethodRelation existedRestInfoMethodRelation = restInfoMethodRelationSet.stream().filter(restInfoMethodRelation::equals).findFirst().orElse(null);
            if (existedRestInfoMethodRelation != null) {
                throw new RestRuntimeException("The restController[{}] method[{}] register requestMappingPath[{}] methodMap[{}] fail, " +
                        "because it's already registered by the restController[{}] method[{}]!",
                        restInfo.getRestControllerClass().getCanonicalName(), restInfo.getRestControllerMethod().getName(), requestMappingPath, methodMap,
                        existedRestInfoMethodRelation.restInfo.getRestControllerClass().getCanonicalName(),
                        existedRestInfoMethodRelation.restInfo.getRestControllerMethod().getName());
            }
        }
        restInfoMethodRelationSet.add(restInfoMethodRelation);
        saveEncryptRestInfo(restInfo);
        return getRegisterRequestMappingPath(methodMap);
    }

    @Nullable
    public RestInfo getRestInfo(String requestMsg) {
        if (priorityRestInfoMethodRelationMap != null) {
            JSONObject jsonObject = JSON.parseObject(requestMsg);
            // 算法说明：这里使用treeMap排序并使用倒序，即method字段越多越靠前，而越靠前的可匹配的要求越高，则靠前匹配的restInfo相似度越高，优先级也越高，找到后无需全部遍历即可找到最匹配的那个
            for (Set<RestInfoMethodRelation> restInfoMethodRelationSet : priorityRestInfoMethodRelationMap.descendingMap().values()) {
                for (RestInfoMethodRelation restInfoMethodRelation : getSortedList(restInfoMethodRelationSet)) {
                    boolean find = true;
                    for (Map.Entry<String, String> methodEntry : restInfoMethodRelation.methodMap.entrySet()) {
                        if (!methodEntry.getValue().equals(jsonObject.get(methodEntry.getKey()))) {
                            find = false;
                            break;
                        }
                    }
                    if (find) {
                        return restInfoMethodRelation.restInfo;
                    }
                }
            }
        }
        return defaultRestInfo;
    }

    RequestMappingPathForRestInfo(String requestMappingPath) {
        this.requestMappingPath = requestMappingPath;
    }

    private void checkEncryptRestInfo(RestInfo restInfo) {
        if (firstRestInfo == null) {
            return;
        }
        if (firstRestInfo.isEncrypt() != restInfo.isEncrypt()
                || !Objects.equals(firstRestInfo.getSecret(), restInfo.getSecret())
                || !Objects.equals(firstRestInfo.getIv(), restInfo.getIv())
                || !Objects.equals(firstRestInfo.getEncryptRequestKey(), restInfo.getEncryptRequestKey())
                || !Objects.equals(firstRestInfo.getEncryptResponseKey(), restInfo.getEncryptResponseKey())) {
            throw new RestRuntimeException("The restController[{}] method[{}] register requestMappingPath[{}] fail, " +
                    "because the method of encrypting information is inconsistent with the restController[{}] method[{}], " +
                    "the same requestMappingPath encryption information must be consistent!",
                    restInfo.getRestControllerClass().getCanonicalName(), restInfo.getRestControllerMethod().getName(), requestMappingPath,
                    firstRestInfo.getRestControllerClass().getCanonicalName(), firstRestInfo.getRestControllerMethod().getName());
        }
    }

    private void saveEncryptRestInfo(RestInfo restInfo) {
        if (firstRestInfo == null) {
            firstRestInfo = restInfo;
        }
    }

    private String getRegisterRequestMappingPath(Map<String, String> methodMap) {
        StringBuilder stringBuilder = new StringBuilder(requestMappingPath);
        stringBuilder.append("?");
        methodMap.forEach((key, value) -> stringBuilder.append(key).append("=").append(value).append("&"));
        String result = stringBuilder.toString();
        return result.substring(0, result.length() - 1);
    }

    private static final class RestInfoMethodRelation {

        private Map<String, String> methodMap;

        private RestInfo restInfo;

        private RestInfoMethodRelation(Map<String, String> methodMap, RestInfo restInfo) {
            this.methodMap = methodMap;
            this.restInfo = restInfo;
        }

        @Override
        public int hashCode() {
            return this.methodMap.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || obj.getClass() != this.getClass()) {
                return false;
            }
            RestInfoMethodRelation restInfoMethodRelation = (RestInfoMethodRelation) obj;
            return this.methodMap.equals(restInfoMethodRelation.methodMap);
        }
    }

    public boolean isEncrypt() {
        checkFirstRestInfo();
        return firstRestInfo.isEncrypt();
    }

    public String getSecret() {
        checkFirstRestInfo();
        return firstRestInfo.getSecret();
    }

    public String getIv() {
        checkFirstRestInfo();
        return firstRestInfo.getIv();
    }

    public String getRequestMappingPath() {
        return requestMappingPath;
    }

    public String getEncryptRequestKey() {
        checkFirstRestInfo();
        return firstRestInfo.getEncryptRequestKey();
    }

    public String getEncryptResponseKey() {
        checkFirstRestInfo();
        return firstRestInfo.getEncryptResponseKey();
    }

    private void checkFirstRestInfo() {
        if (firstRestInfo == null) {
            throw new RestRuntimeException("The rest info for '{}' can't be found!", requestMappingPath);
        }
    }

    private List<RestInfoMethodRelation> getSortedList(Set<RestInfoMethodRelation> set) {
        List<RestInfoMethodRelation> restInfoMethodRelationList = new ArrayList<>(set);
        restInfoMethodRelationList.sort(Comparator.comparingInt(r -> r.restInfo.getMethodPriority()));
        return restInfoMethodRelationList;
    }
}